• 将数据封装在对象内部,可以将数据的访问限制在对象的方法上,从而更容易确保线程在访问数据时总能持有正确的锁。

    看下面的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public class PersonSet {
    private final Set<Person> mySet = new HashSet<Person>(); // HashSet不是线程安全的,但由于mySet是私有的,并且不会逸出,因此HashSet被封装在PersonSet中
    public synchronized void addPerson(Person p) {
    mySet.add(p);
    }
    public synchronized boolean containsPerson(Person p) {
    return mySet.contains(p);
    }
    interface Person {
    }
    }

    PersonSet的状态由HashSet来管理,而HashSet不是线程安全的,但由于mySet是私有的并且不会逸出,因此HashSet被封装在PersonSet中。唯一能访问mySet的代码路径是addperson和containsPerson,在执行它们时需要获得PersonSet上的锁(this),-PersonSet的状态完全由它的内置锁保护,因此PersonSet是一个线程安全的类。

    这个实力并未对Person的线程安全型做任何假设,但如果Person类是可变的,那么在访问从PersonSet中获得的Person对象时,还需要额外的同步,要想安全的使用Person对象,最可靠的方法是使Person成为一个线程安全的类。

  • 实例封闭是构建线程安全类的一个最简单的方式,实例封装还使得不同的状态变量可以由不同的锁来保护。

  • Java的内置锁也称为监视器锁或监视器

  • 线程安全性的委托

    我们在一个无状态的类中增加一个AtomicLong类型的域,由于AtomicLong是线程安全的,因此该类也是一个线程安全的类,因此我们可以说该类将它的线程安全性委托给AtomicLong来保证。

  • 独立的状态变量

    我们还可以将线程安全委托给多个状态变量,只要这些变量是彼此独立的,即组合而成的类并不在其包含的多个状态变量上增加任何不变性条件。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public class VisualComponent {
    private final List<KeyListener> keyListeners
    = new CopyOnWriteArrayList<KeyListener>(); //CopyOnWriteArrayList是线程安全的链表
    private final List<MouseListener> mouseListeners
    = new CopyOnWriteArrayList<MouseListener>();//两个状态变量是彼此独立的
    public void addKeyListener(KeyListener listener) {
    keyListeners.add(listener);
    }
    public void addMouseListener(MouseListener listener) {
    mouseListeners.add(listener);
    }
    public void removeKeyListener(KeyListener listener) {
    keyListeners.remove(listener);
    }
    public void removeMouseListener(MouseListener listener) {
    mouseListeners.remove(listener);
    }
    }
  • 在现有的线程安全类中添加功能:

    • 继承这个线程安全的类

      看下面的代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class BetterVector<E> extentds vector<E> {
      static final long serialVersionUID = -3963416950630760754L;
      public synchronized boolean putIfAbsent(E x) {
      boolean absent = !contains(x);
      if (absent) {
      add(x);
      }
      return absent;
      }
      }

      继承方法存在的问题就是:

      如果底层的类改变了同步策略并选择了不同的锁来保护它的状态变量,那么子类就会被破坏,因此在同步策略改变后它无法再使用正确的锁来控制对基类状态的并发访问。

      在上面的例子中,如果vector底层不再使用synchronized对contains()和add()方法加锁,那么putIfAbsent()方法就不是线程安全的。

    • 扩展类的功能,但并不是继承类本身,而是将类放在一个扩展类中

      看下面的代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public BadListHelper<E> {
      public List<E> list = Collections.synchronizedList(new ArrayList<E>());
      public synchronized boolean putIfAbsent(E x) {
      boolean absent = !list.contains(x);
      if (absent) {
      list.add(x);
      }
      return absent;
      }
      }

      上面这个类并不是线程安全的,问题在于在错误的锁上进行了同步,list使用的同步锁并不是BadListHelper上的锁。

      可以将上面的代码改成下面的形式:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class GoodListHelper<E> {
      public List<E> list = Collections.synchronizedList(new ArrayList<E>());
      public boolean putIfAbsent(E x) {
      synchronized(list) {
      boolean absent = !list.contains(x);
      if (absent) {
      list.add(x);
      }
      return absent;
      }
      }
      }

    继承会破坏实现的封装性,客户端加锁会破坏同步策略的封装性。

  • 当为现有的类添加一个原子操作时,有一种更好的方法就是组合。

    看下面的代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class ImprovedList<T> implements List<T> {
    private final List<T> list;
    public ImprovedList(List<T> list) {
    this.list = list;
    }
    public synchronized boolean putIfAbsent(T x) {
    boolean contains = list.contains(x);
    if (contains) {
    list.add(x);
    }
    return !contains;
    }
    }

    ImprovedList通过将List对象委托给底层的List实例来实现List的操作,同时还添加了一个原子的putIfAbsent方法。

    ImprovedList通过自身的内置锁增加了一层额外的加锁,它并不关心底层的List是否是线程安全的。
    事实上,我们使用了Java监视器模式来封装现有的List,并且只要在类中拥有指向底层List的唯一外部引用,就能确保线程安全性。

  • 本文参考自Java并发编程实战(学习笔记三 第四章 对象的组合)